﻿using System;
using System.Collections.Generic;
using System.Text;
using SlimDX;
using SlimDX.Direct3D9;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Windows.Forms;

namespace Terrain
{
	struct CustomVertex
	{
		public Vector3 Position;
		public Vector3 Normal;
		public Vector2 TexCoord;
		public Vector4 TexBlender;

		public CustomVertex(Vector3 position, Vector3 normal, Vector2 texCoord, Vector4 texBlend)
		{
			this.Position = position;
			this.Normal = normal;
			this.TexCoord = texCoord;
			this.TexBlender = texBlend;
		}
	}

	class Terrain
	{
		///mapa to kwadrat o boku TSize	
		protected int tSize;
		///ilosc wierzchołków siatki mapy
		protected long vertexamount;
		///ilość poligonów (trójkątów) mapy
		protected long polyamount;
		///zerowa, stała macierz przesunięcia
		protected Matrix matWorld;
		///materiał modelu
		protected Material tMaterial;
		///obiekt pomocny przy programach jednostek cieniujacych
		protected Effect effect;

		private Texture grass, sand, rock, snow;
		///kontekst urzadzenia
		protected Device device;
		///heightmap zapisana jako jednywymiarowa tablica float'ów
		private float[] tMap;
		///punkty okreslajace granice dna otaczajacego mapę
		private float[] tSeaBed;

		private VertexDeclaration vertexDeclaration;
		///bufor wierzchołków 
		protected VertexBuffer mTerrainVB;
		///bufor indeksów   
		protected IndexBuffer mTerrainIB;
		
		//------------------------------------------------------------

		///Konstrukor obiektu terenu
		public Terrain(Device device, int size)
		{
			this.device = device;
			matWorld = Matrix.Identity;
			tSize = size;
			tMap = null;
			tMaterial = new Material();
			tMaterial.Ambient = Color.Black;
			tMaterial.Diffuse = Color.White;
			vertexDeclaration = new VertexDeclaration(device, new[] {
								new VertexElement(0, 0, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Position, 0), 
								new VertexElement(0, 12, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Normal, 0), 
								new VertexElement(0, 24, DeclarationType.Float2, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 0), 
								new VertexElement(0, 32, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 1), 
				VertexElement.VertexDeclarationEnd
					});
			
			mTerrainVB = null;
			mTerrainIB = null;
		}
		//------------------------------------------------------------    
		///Nadpisana metoda wywołuje poniższą funkcje z domyślnymi argumentami
		public void CreateTerrain()
		{
			CreateTerrain(1.0f, 0);
		}
		///Wykonuje cały proces generowania mapy - heightmap 2d, wygładzanie, obliczanie wektorów normalnych, wypełnianie bufora wierzchołków i indeksów, wczytywanie shaderów.
		public void CreateTerrain(float vertDiversity, int smoothpasses)
		{
			effect = Effect.FromFile(device, "../../terrain.fx", ShaderFlags.None);
			grass = Texture.FromFile(device, "../../grass.png");
			rock = Texture.FromFile(device, "../../rock.png");
			sand = Texture.FromFile(device, "../../sand.png");
			snow = Texture.FromFile(device, "../../snow.png");

			vertexamount = tSize * tSize;			//tyle vertexow
			polyamount = (tSize - 1) * (tSize - 1) * 2;	//tyle bedzie poligonow 
			tMap = new float[tSize * tSize];		//tworze mape wysokosci w tablicy jednowymiarowej (latwiej przepisac do buforow)
			tSeaBed = new float[8];

			//CreateSeaBed();
			CreateHeightMap(vertDiversity, smoothpasses);//algorytm tworzenia mapy wysokosci                    

			FillBuffers();//wypełnianie buforów, liczenie wektorów normalnych i wczytywanie shaderów
		}

		///Rysowanie mapy
		public void Draw()
		{
			device.VertexDeclaration = vertexDeclaration;

			Matrix view = device.GetTransform(TransformState.View);
			Matrix projection = device.GetTransform(TransformState.Projection);
			Matrix worldViewProj = matWorld * view * projection;

			device.SetStreamSource(0, mTerrainVB, 0, Marshal.SizeOf(typeof(CustomVertex)));
			device.Indices = mTerrainIB;
			device.VertexDeclaration = vertexDeclaration;

			effect.Technique = "Simple";
			effect.SetValue("xWorldViewProj", worldViewProj);


			Light light = device.GetLight(0);
			effect.SetValue("xMaterialAmbient", this.tMaterial.Ambient);
			effect.SetValue("xMaterialDiffuse", this.tMaterial.Diffuse);
			effect.SetValue("xMaterialSpecular", this.tMaterial.Specular);
			effect.SetValue("xLight", new Vector4(Vector3.Normalize(light.Direction), 1.0f));
			effect.SetValue("xLightPos", new Vector4(light.Position, 1.0f));
			effect.SetValue("xLightAmbient", light.Ambient);
			effect.SetValue("xLightDiffuse", light.Diffuse);
			effect.SetValue("xLightSpecular", light.Specular);


			effect.SetTexture("sandtex", sand);
			effect.SetTexture("rocktex", rock);
			effect.SetTexture("snowtex", snow);
			effect.SetTexture("grasstex", grass);

			//{

			effect.Begin(0);
			effect.BeginPass(0);
			{
				device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, (int)vertexamount, 0, (int)polyamount);
			}
			effect.EndPass();
			effect.End();
		}
		///Zwraca przybliżoną wysokość powierzchni mapy w punkcie (x,z)


		///Wypełnianie buforów, liczenie wektorów normalnych i wczytywanie shaderów
		private void FillBuffers()
		{
			//bufor wierzchołków:
			mTerrainVB = new VertexBuffer(device, (int)vertexamount * Marshal.SizeOf(typeof(CustomVertex)),
								Usage.WriteOnly, VertexFormat.None, Pool.Managed);

			CustomVertex[] vbData;
			vbData = new CustomVertex[vertexamount];
			int help;
			for (int i = 0; i < tSize; i++)
			{
				for (int j = 0; j < tSize; j++)
				{
					help = j * tSize + i; //indeks w tablicy
					float normalizator = 0;
					vbData[help].Position.X = (float)3 * j; //wektor pozycji
					vbData[help].Position.Y = (float)tMap[help];
					vbData[help].Position.Z = (float)3 * i;
					if (tMap[help] >= 120.0f) vbData[help].TexBlender.W = 1;
					else if (tMap[help] <= -10.0f) vbData[help].TexBlender.X = 1;
					else
					{
						normalizator = vbData[help].TexBlender.X = Clamp(1.0f - Math.Abs(tMap[help] - 0) / 10.0f, 0, 1);
						normalizator += vbData[help].TexBlender.Y = Clamp(1.0f - Math.Abs(tMap[help] - 40) / 30.0f, 0, 1);
						normalizator += vbData[help].TexBlender.Z = Clamp(1.0f - Math.Abs(tMap[help] - 80) / 20.0f, 0, 1);
						normalizator += vbData[help].TexBlender.W = Clamp(1.0f - Math.Abs(tMap[help] - 100) / 20.0f, 0, 1);

						vbData[help].TexBlender.X /= normalizator;
						vbData[help].TexBlender.Y /= normalizator;
						vbData[help].TexBlender.Z /= normalizator;
						vbData[help].TexBlender.W /= normalizator;
					}

					//textury
					vbData[help].TexCoord.X = (float)i / 16.0f;//(i%2); //detal, kafelkowana
					vbData[help].TexCoord.Y = (float)j / 16.0f;//(j % 2);
				}
			}

			CalculateNormals(vbData);

			var stream = mTerrainVB.Lock(0, 0, LockFlags.None);
			stream.WriteRange(vbData);
			mTerrainVB.Unlock();

			mTerrainIB = new IndexBuffer(device, (int)polyamount * 3 * 4, Usage.None, Pool.Default, false);

			int[] mIndexData;
			mIndexData = new int[polyamount * 3];

			//wypelniam bufor indeksow w takiej kolejnosci:
			//  v1---v2
			//  |   /|
			//  |  / |
			//  | /  |
			//  v3---v4*/
			int counter = 0;
			for (int i = 1; i < tSize; i++) //zaczynam od drugiego wiersza 
			{
				for (int j = 0; j < tSize - 1; j++)	//koncze przed ostatnia kolumna
				{
					//dolny polygon
					mIndexData[counter++] = i * tSize + j;      //v3
					mIndexData[counter++] = i * tSize + j + 1;	//v4
					mIndexData[counter++] = (i - 1) * tSize + j + 1;//v2
					//gorny polygon
					mIndexData[counter++] = i * tSize + j;		//v3
					mIndexData[counter++] = (i - 1) * tSize + j + 1;//v2
					mIndexData[counter++] = (i - 1) * tSize + j;	//v1	
				}
			}

			stream = mTerrainIB.Lock(0, 0, LockFlags.None);
			stream.WriteRange(mIndexData);
		}
		///tworzenie mapy wyskosci w TMap + jej n-krotne wygładzanie
		private void CreateHeightMap(float vertDiversity, int smoothpasses)
		{
			Random myRandom = new Random();
			for (int i = 0; i < vertexamount; i++) tMap[i] = 0.0f; //zerujemy na początku

			Vector2 p1, p2;
			Vector2 v1, v2;
			float zVect;
			int zoneReduct;
			for (int k = 0; k < 600; k++)
			{
				zoneReduct = k % tSize / 2 + 1;
				p1.X = myRandom.Next(tSize); //dowolne wartości 0-TSize
				p1.Y = myRandom.Next(tSize);
				p2.X = myRandom.Next(tSize);
				p2.Y = myRandom.Next(tSize);

				v1.X = (float)p2.X - p1.X;
				v1.Y = (float)p2.Y - p1.Y;

				for (int i = zoneReduct; i < tSize - zoneReduct; i++)
				{
					for (int j = zoneReduct; j < tSize - zoneReduct; j++)
					{

						//tMap[i * tSize + j] = (float)Math.Cos(i)*4;
						v2.X = (float)j - p1.X;
						v2.Y = (float)i - p1.Y;

						zVect = (float)(v1.X * v2.Y) - (v2.X * v1.Y);

						if (zVect > 0) tMap[i * (tSize) + j] += vertDiversity;
						else tMap[i * (tSize) + j] -= vertDiversity;
					}
				}
			}

			for (int i = 0; i < smoothpasses; i++) SmoothMap();
			for (int i = 0; i < vertexamount; i++) tMap[i] -= 10.0f;
		}
		///oblicznie wektorow normalnych w wierzchołkach
		private void CalculateNormals(CustomVertex[] vertices)
		{
			for (int j = 0; j < tSize; j++) //tutaj zamieniłem i<==>j
			{
				for (int i = 0; i < tSize; i++) //tutaj zamieniłem i<==>j
				{
					if ((i > 0 && i < (tSize - 1) && j > 0 && j < (tSize - 1)))
					{
						//mVertices[i * tSize + j].Normal = GetNormal(i*3, j*3);

						Vector3 PolyNorm, vNormal = new Vector3(0, 0, 0), v1, v2;
						//1
						v1 = vertices[i * tSize + j + 1].Position - vertices[i * tSize + j].Position;
						v2 = vertices[(i - 1) * tSize + j + 1].Position - vertices[i * tSize + j].Position;

						PolyNorm = Vector3.Cross(v2, v1);
						vNormal += PolyNorm;
						//2
						v1 = vertices[(i - 1) * tSize + j + 1].Position - vertices[i * tSize + j].Position;
						v2 = vertices[(i - 1) * tSize + j].Position - vertices[i * tSize + j].Position;

						PolyNorm = Vector3.Cross(v2, v1);
						vNormal += PolyNorm;
						//3
						v1 = vertices[(i - 1) * tSize + j].Position - vertices[i * tSize + j].Position;
						v2 = vertices[(i) * tSize + j - 1].Position - vertices[i * tSize + j].Position;

						PolyNorm = Vector3.Cross(v2, v1);
						vNormal += PolyNorm;
						//4
						v1 = vertices[(i) * tSize + j - 1].Position - vertices[i * tSize + j].Position;
						v2 = vertices[(i + 1) * tSize + j - 1].Position - vertices[i * tSize + j].Position;


						PolyNorm = Vector3.Cross(v2, v1);
						vNormal += PolyNorm;
						//5
						v1 = vertices[(i + 1) * tSize + j - 1].Position - vertices[i * tSize + j].Position;
						v2 = vertices[(i + 1) * tSize + j].Position - vertices[i * tSize + j].Position;

						PolyNorm = Vector3.Cross(v2, v1);
						vNormal += PolyNorm;
						//6
						v1 = vertices[(i + 1) * tSize + j].Position - vertices[i * tSize + j].Position;
						v2 = vertices[(i) * tSize + j + 1].Position - vertices[i * tSize + j].Position;

						PolyNorm = Vector3.Cross(v2, v1);
						vNormal += PolyNorm;
						vNormal.Normalize();
						vertices[i * tSize + j].Normal = vNormal;
					}
					else vertices[i * tSize + j].Normal = new Vector3(0, 1, 0);
				}
			}
		}

		///Wygładzanie powierzchni mapy (uśrednianie wg najbliższych sąsiadów wierzchołka)
		private void SmoothMap()
		{
			for (int i = 0; i < tSize; i++)
			{
				for (int j = 0; j < tSize; j++)
				{
					if (i > 0 && i < (tSize - 1) && j > 0 && j < (tSize - 1))
					{
						//średnia wysokości z wszystkich 8 sąsiadującyh wierzchołków
						tMap[i * tSize + j] = (tMap[i * tSize + j - 1] + tMap[(i - 1) * tSize + j - 1] +
																		tMap[(i - 1) * tSize + j] + tMap[(i - 1) * tSize + j + 1] +
																		tMap[i * tSize + j + 1] + tMap[(i + 1) * tSize + j + 1] +
																		tMap[(i + 1) * tSize + j] + tMap[(i + 1) * tSize + j - 1] +
																		tMap[i * tSize + j]) / 8;
					}
				}
			}
		}

		private T Clamp<T>(T value, T min, T max)
				where T : System.IComparable<T>
		{
			T result = value;
			if (value.CompareTo(max) > 0)
				result = max;
			if (value.CompareTo(min) < 0)
				result = min;
			return result;
		}
	}
}